﻿using System.Diagnostics.Contracts;
using System.Linq;
using System.Reactive;
using System.Reflection;
using System.Windows.Input;
using System.Windows.Markup;
using Rxx.Properties;

/* This namespace is used for consistency with the WPF version of this class. */
namespace System.Windows.Reactive
{
	/// <summary>
	/// Represents a trigger that, in response to an event, invokes a handler that is either an <see cref="EventHandler"/> delegate, 
	/// an object that implements <see cref="ICommand"/> or an object that implements <see cref="IObserver{T}"/> where <strong>T</strong> 
	/// is a compatible <see cref="EventPattern{TEventArgs}"/>.
	/// </summary>
	/// <remarks>
	/// <alert type="warning">
	/// <see cref="EventSubscription"/> may only be used in a <see cref="FrameworkElement.Triggers"/> collection; otherwise, the specified 
	/// <see cref="Handler"/> will never be called.
	/// </alert>
	/// </remarks>
	[ContentProperty("")]
	public sealed class EventSubscription : System.Windows.Interactivity.EventTrigger
	{
		#region Public Properties
		/// <summary>
		/// Identifies the <see cref="Handler"/> dependency property.
		/// </summary>
		/// <value>The identifier for the <see cref="Handler"/> dependency property.</value>
		public static readonly DependencyProperty HandlerProperty = DependencyProperty.Register(
			"Handler",
			typeof(object),
			typeof(EventSubscription),
			new PropertyMetadata(null, HandlerChanged));

		/// <summary>
		/// Gets or sets the reference to a handler for the specified <see cref="System.Windows.Interactivity.EventTrigger.EventName"/>.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The supported handler types are as follows:
		/// </para>
		/// <list type="bullet">
		/// <item>
		/// <see cref="IObserver{T}"/> where <strong>T</strong> is <see cref="EventPattern{TEventArgs}"/> and <strong>TEventArgs</strong> 
		/// is convertible contravariantly to the argument type of the specified event.  See below for more information.
		/// </item>
		/// <item>
		/// <see cref="Delegate"/> with a matching contravariant signature.  See below for more information.
		/// </item>
		/// <item>
		/// Any implementation of <see cref="ICommand"/>.  The command parameter will be an instance of 
		/// <see cref="EventPattern{TEventArgs}"/> where <strong>TEventArgs</strong> is the argument type 
		/// of the specified event.
		/// </item>
		/// </list>
		/// <para>
		/// Contravariance is supported for the parameters of an <see cref="EventHandler"/> delegate.  This means that the 
		/// delegate's <em>sender</em> parameter can be the exact type or any base class of the <see cref="FrameworkElement"/> that raises 
		/// the specified event.  Furthermore, the <em>EventArgs</em> parameter can be the actual argument type of the 
		/// specified event or any base class.
		/// </para>
		/// <para>
		/// Likewise, contravariance is supported for the generic type argument of an object that implements <see cref="IObserver{T}"/> where 
		/// <strong>T</strong> is <see cref="EventPattern{TEventArgs}"/> and <strong>TEventArgs</strong> can be the actual argument type of the 
		/// specified event or any base class to <see cref="EventArgs"/>.
		/// </para>
		/// </remarks>
		/// <value>Reference to the handler that is attached by this trigger.</value>
		public object Handler
		{
			get
			{
				return GetValue(HandlerProperty);
			}
			set
			{
				SetValue(HandlerProperty, value);
			}
		}
		#endregion

		#region Private / Protected
		private EventHandler handlerProxy;
		private Type eventPatternType;
		private ICommand command;
		private object observer;
		private Type observerPatternType;
		private MethodInfo onNext;
		private Type previousEventArgsType;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="EventSubscription" /> class.
		/// </summary>
		public EventSubscription()
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="EventSubscription" /> class with the specified <paramref name="eventName"/>.
		/// </summary>
		/// <param name="eventName">The name of the event to be handled.</param>
		public EventSubscription(string eventName)
			: base(eventName)
		{
			Contract.Requires(!string.IsNullOrEmpty(eventName));
		}
		#endregion

		#region Methods
		private static void HandlerChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
		{
			Contract.Requires(obj != null);

			var trigger = (EventSubscription) obj;

			trigger.Reset();
		}

		/// <summary>
		/// Called when the source property changes.
		/// </summary>
		/// <param name="oldSource">The old source.</param>
		/// <param name="newSource">The new source.</param>
		protected override void OnSourceChanged(object oldSource, object newSource)
		{
			base.OnSourceChanged(oldSource, newSource);

			Reset();
		}

		private void Reset()
		{
			handlerProxy = null;
			eventPatternType = null;
			command = null;
			observer = null;
			observerPatternType = null;
			onNext = null;
		}

		/// <summary>
		/// Called when the event is raised.
		/// </summary>
		/// <param name="eventArgs">Arguments for the event.</param>
		protected override void OnEvent(EventArgs eventArgs)
		{
			Contract.Assume(eventArgs != null);

			base.OnEvent(eventArgs);

			var d = Handler;

			if (d != null)
			{
				var source = Source;
				var eventArgsType = eventArgs.GetType();

				if (handlerProxy == null || eventArgsType != previousEventArgsType)
				{
					handlerProxy = Convert(source, eventArgs, d);
				}

				previousEventArgsType = eventArgsType;

				handlerProxy(source, eventArgs);
			}
		}

		[ContractVerification(false)]
		private EventHandler Convert(object source, EventArgs eventArgs, object value)
		{
			Contract.Requires(eventArgs != null);
			Contract.Requires(value != null);
			Contract.Ensures(Contract.Result<Delegate>() != null);

			EventHandler proxy = null;

			var handler = value as Delegate;

			if (handler != null)
			{
				EnsureDelegateIsCompatible(source, eventArgs, handler);

				proxy = (s, e) => handler.DynamicInvoke(s, e);
			}
			else
			{
				eventPatternType = typeof(EventPattern<>).MakeGenericType(eventArgs.GetType());

				command = value as ICommand;

				if (command != null)
				{
					proxy = ExecuteCommand;
				}
				else
				{
					proxy = TryCreateObserverProxy(eventArgs, value);
				}
			}

			if (proxy != null)
			{
				return proxy;
			}

			throw new InvalidOperationException(Errors.EventSubscriptionIncompatibleHandler);
		}

		private static void EnsureDelegateIsCompatible(object sender, EventArgs eventArgs, Delegate handler)
		{
			Contract.Requires(eventArgs != null);
			Contract.Requires(handler != null);

			var invoke = handler.GetType().GetMethod("Invoke");

			if (invoke != null)
			{
				var parameters = invoke.GetParameters();

				if (parameters.Length == 2)
				{
					Contract.Assume(parameters[0] != null);
					Contract.Assume(parameters[1] != null);

					var targetSenderType = parameters[0].ParameterType;

					if ((sender == null && !targetSenderType.IsValueType)
						|| (sender != null && targetSenderType.IsAssignableFrom(sender.GetType())))
					{
						var targetEventArgsType = parameters[1].ParameterType;

						if (targetEventArgsType.IsAssignableFrom(eventArgs.GetType()))
						{
							return;
						}
					}
				}
			}

			throw new InvalidOperationException(Errors.EventSubscriptionIncompatibleDelegate);
		}

		private EventHandler TryCreateObserverProxy(EventArgs eventArgs, object value)
		{
			Contract.Requires(eventArgs != null);
			Contract.Requires(eventPatternType != null);
			Contract.Requires(value != null);

			var type = value.GetType();

			var observerType = type.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IObserver<>));

			if (observerType != null)
			{
				var arguments = observerType.GetGenericArguments();

				Contract.Assume(arguments.Length == 1);

				var observerArgument = arguments[0];

				Contract.Assume(observerArgument != null);

				if (eventPatternType.IsAssignableFrom(observerArgument))
				{
					observerPatternType = eventPatternType;

					goto Bind;
				}
				else if (observerArgument.IsGenericType && observerArgument.GetGenericTypeDefinition() == typeof(EventPattern<>))
				{
					arguments = observerArgument.GetGenericArguments();

					Contract.Assume(arguments.Length == 1);

					var targetEventArgument = arguments[0];

					Contract.Assume(targetEventArgument != null);

					arguments = eventPatternType.GetGenericArguments();

					Contract.Assume(arguments.Length == 1);

					var actualEventArgument = arguments[0];

					Contract.Assume(actualEventArgument != null);

					if (targetEventArgument.IsAssignableFrom(actualEventArgument))
					{
						observerPatternType = observerArgument;

						goto Bind;
					}
				}

				throw new InvalidOperationException(Errors.EventSubscriptionIncompatibleObserver);
			}

			return null;

		Bind:
			observer = value;

			var map = observer.GetType().GetInterfaceMap(observerType);

			Contract.Assume(map.TargetMethods != null);

#if SILVERLIGHT
			Contract.Assume(map.TargetMethods.Count() > 0);
#endif

			onNext = map.TargetMethods.First(method => method.Name == "OnNext");

			return NotifyObserver;
		}

		private void ExecuteCommand(object sender, EventArgs e)
		{
			Contract.Requires(command != null);
			Contract.Requires(eventPatternType != null);

			var @event = Activator.CreateInstance(eventPatternType, sender, e);

			if (command.CanExecute(@event))
			{
				command.Execute(@event);
			}
		}

		private void NotifyObserver(object sender, EventArgs e)
		{
			Contract.Requires(observer != null);
			Contract.Requires(observerPatternType != null);
			Contract.Requires(onNext != null);

			var @event = Activator.CreateInstance(observerPatternType, sender, e);

			onNext.Invoke(observer, new[] { @event });
		}
		#endregion
	}
}